/*
   Copyright 2011-2012 gtalent2@gmail.com

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

     http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/
package world

import (
	"../models"
	eutil "../util"
	"errors"
	"github.com/gtalent/starfish/util"
	"runtime"
	"sync"
)

type Zone struct {
	world         *World
	tiles         [][][]Tile
	scripts       []*interpreter
	scriptAdder   chan *interpreter
	scriptRemover chan *interpreter
	location      util.Point
	size          util.Size
	lock          sync.Mutex
	loaded        bool
	running       bool
	path          string
	sprites       map[string]Sprite
	tileClaimsIn  chan func() bool //used to process claiming of tiles in a thread safe manner
	tileClaimsOut chan bool        //used to return success indicators of tile claims
}

func buildZone(data models.ZoneInstance, world *World) *Zone {
	path := data.Path
	z := new(Zone)
	var h models.ZoneHeader
	eutil.LoadObj(path, &h)

	z.scriptAdder = make(chan *interpreter)
	z.scriptRemover = make(chan *interpreter)

	go func() {
		for {
			select {
			case in, ok := <-z.scriptRemover:
				if !ok {
					return
				}
				index := -1
				for i, v := range z.scripts {
					if v == in {
						index = i
						break
					}
				}
				if index == -1 {
					continue
				}
				z.scripts[index] = z.scripts[len(z.scripts)-1]
				z.scripts = z.scripts[:len(z.scripts)-1]
			case in, ok := <-z.scriptAdder:
				if !ok {
					return
				}
				z.scripts = append(z.scripts, in)
			}
		}
	}()

	z.tileClaimsIn = make(chan func() bool)
	z.tileClaimsOut = make(chan bool)
	go func() {
		for {
			f, ok := <-z.tileClaimsIn
			if ok {
				z.tileClaimsOut <- f()
			} else {
				break
			}
		}
	}()
	z.world = world
	z.location = data.Location
	z.size = h.Size
	z.path = h.Path
	z.loaded = false
	z.sprites = make(map[string]Sprite)
	return z
}

func (me *Zone) getTile(x, y, z int) *Tile {
	return &me.tiles[z][y][x]
}

func (me *Zone) loadSprite(sprite models.Sprite, x, y, layer int) error {
	s, err := loadSprite(sprite)
	if err != nil {
		return err
	}
	if !me.getTile(x, y, layer).claim(s, me) {
		return errors.New("Tile already occupied.")
	}
	if sprite.Name != "" {
		me.sprites[sprite.Name] = s
	}
	s.SetLocation(tileSize.Width * x, tileSize.Height * y)
	s.SetLayer(layer)
	return nil
}

func (me *Zone) deleteSprite(sprite Sprite) {
	delete(me.sprites, sprite.Name())
	for {
		sprite.lock()
		if sprite.isMoving() {
			sprite.unlock()
			runtime.Gosched()
			continue
		}
		loc := sprite.Location().DivideOf(tileSize.ToPoint())
		tile := me.getTile(loc.X, loc.Y, sprite.Layer())
		tile.occupant = nil
		sprite.unlock()
		break
	}
}

func (me *Zone) AddUser(user *User, name string, x, y, layer int) error {
	if !me.loaded {
		me.load()
	}
	if !me.getTile(x, y, layer).claim(user, me) {
		return errors.New("Tile already occupied.")
	}
	if name != "" {
		me.sprites[name] = user
	}
	user.tile = me.getTile(x, y, layer)
	user.zone = me
	user.SetLocation(tileSize.Width * x, tileSize.Height * y)
	user.SetLayer(layer)
	return nil
}

func (me *Zone) Layers() int {
	return len(me.tiles)
}

func (me *Zone) load() error {
	me.lock.Lock()
	if me.loaded {
		me.lock.Unlock()
		return nil
	}

	spec := new(models.Zone)
	err := eutil.LoadObj(me.path, spec)
	if err != nil {
		return err
	}
	//Fill out tiles...
	me.tiles = make([][][]Tile, len(spec.Tiles))
	//build the matrix
	for i, v := range spec.Tiles {
		me.tiles[i] = make([][]Tile, len(v))
		for ii, vv := range v {
			me.tiles[i][ii] = make([]Tile, len(vv))
		}
	}
	//populate the matrix
	for i, v := range spec.Tiles {
		for ii, vv := range v {
			for iii, ts := range vv {
				me.getTile(iii, ii, i).build(ts)
				if ts.Occupant.Name != "" {
					me.sprites[ts.Occupant.Name] = me.getTile(iii, ii, i).occupant
				}
				if ts.Occupant.ScriptPath != "" {
					script, err := loadScript(ts.Occupant.ScriptPath)
					if err != nil {
						return err
					}
					me.scripts = append(me.scripts, new_interpreter(script))
				}
			}
		}
	}
	me.loaded = true
	me.lock.Unlock()

	//run init scripts
	interpreters := make([]*interpreter, len(spec.InitScripts))
	for i, v := range spec.InitScripts {
		script, err := loadScript(v)
		if err != nil {
			return err
		}
		interpreters[i] = new_interpreter(script)
		go interpreters[i].run()
	}

	for _, v := range interpreters {
		v.done()
	}

	me.running = true
	//start the runtime scripts
	for _, v := range me.scripts {
		go v.run()
	}
	return nil
}

func (me *Zone) stop() {
	me.running = false
	done := make(chan interface{}, len(me.scripts))
	for _, v := range me.scripts {
		go func() {
			v.stop()
			done <- nil
		}()
	}
	for _, _ = range me.scripts {
		<-done
	}
}

func (me *Zone) Free() {
	for _, v := range me.tiles {
		for _, vv := range v {
			for i, _ := range vv {
				vv[i].Free()
			}
		}
	}
	me.loaded = false
	close(me.tileClaimsIn)
}

//Gets the Tile that contains the given point.
func (me *Zone) tileAt(x, y, layer int) *Tile {
	return me.getTile(x/tileSize.Width, y/tileSize.Height, layer)
}

func (me *Zone) SetAddress(addr util.Point) {
	me.location.MultiplyBy(addr)
}

func (me *Zone) Intersects(b util.Bounds) bool {
	mb := me.GetBounds()
	return mb.Intersects(b)
}

func (me *Zone) GetBounds() util.Bounds {
	var b util.Bounds
	b.Point = me.location
	b.Width = len(me.tiles[0][0]) * tileSize.Width
	if len(me.tiles[0]) != 0 {
		b.Height = len(me.tiles[0]) * tileSize.Height
	} else {
		b.Height = 0
	}
	return b
}
